
package com.studiofortress.sf.graphics;

import com.studiofortress.sf.util.ArrayUtil;
import java.util.LinkedList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * This manages storing textures all together in one global place. This allows
 * textures to be loaded before use, reloaded or changed on a global level
 * across the entire application.
 *
 * Internally textures are mapped by their path to an integer that represents
 * where they are stored.
 *
 * Unless you want to be involved with managing textures, this class should be
 * ignored and GLTexture can be used to 'conceptually' act like that is a
 * texture.
 *
 * The auto unload feature states if a texture is unloaded from the system once
 * it is no longer used. If this is set to true (which is the default) then it
 * will be. If set to false then it never lets go of a texture, regardless of if
 * it's in use or not. Setting it to false is useful for keeping textures that
 * you know will be used in the future.
 * 
 * Textures can be automatically unloaded using the unload method. It only
 * unloads textures that are not in use.
 * 
 * @author Joseph Lenton
 * @version 13/12/2008
 */
public class GLTextureManager
{
    private static final int DEFAULT_TEXTURE_SIZE = 10;

    private LinkedList<GLTextureInner> textureInternalInitializeQueue;

    private Map<String, Integer> textureNames;

    // this contains the loaded textures. If a texture is unloaded then it's
    // position is then set to null.
    private GLTextureInner[] textures;
    // counts the number of references to the texture at the same index
    private int[] references;
    
    // this is the number of textures stored. as some values in textures
    // might be null this may not be equal to the length of textures
    private int numTextures;

    // States if this should automatically unload a texture out of the systems
    // when it is no longer used.
    // By default this is true.
    private boolean autoUnload;

    /**
     * This class is not designed to be initialised outside of the API.
     * This is a trivial contrustor which creates up the data structures for
     * this texture manager.
     */
    GLTextureManager()
    {
        textureNames = new HashMap<String, Integer>();
        textures = new GLTextureInner[DEFAULT_TEXTURE_SIZE];
        references = new int[DEFAULT_TEXTURE_SIZE];
        textureInternalInitializeQueue = new LinkedList<GLTextureInner>();

        assert(textures.length == references.length);

        autoUnload = false;
    }

    /**
     * If set to true then any texture not in use will be unloaded once it is
     * no longer referenced. If set to false then they are not.
     * The default is false.
     * @param autoUnload True to automatically unload a texture when it is no longer in use, false to not.
     */
    public void setAutoUnload(boolean autoUnload)
    {
        this.autoUnload = autoUnload;
    }

    /**
     * @return True if this manager will unload any texture once it is no longer used, otherwise false.
     */
    public boolean isAutoUnload()
    {
        return autoUnload;
    }
    
    /**
     * This is an internal call for accessing textures.
     * @param index The texture to retrieve.
     * @return The texture stored at the given index.
     */
    final GLTextureInner getTexture(final int index)
    {
        assert(index > 0);
        assert(index < textures.length);

        return textures[index];
    }

    /**
     * Stores the given texture in this GLTextureManager. This is stored at an
     * index which is then returned.
     * @param texture The texture to store. Cannot be null.
     * @return The index where the given texture is stored.
     */
    final int addTexture(final GLTextureInner texture)
    {
        assert(texture != null);
        assert(numTextures <= textures.length);

        if (textures.length > numTextures) {
            if (textures[numTextures] == null) {
                textures[numTextures] = texture;
                references[numTextures] = 0;
                return numTextures++;
            } else {
                for (int i = 0; i < numTextures; i++) {
                    if (textures[i] == null) {
                        textures[i] = texture;
                        references[i] = 0;
                        return i;
                    }
                }

                throw new IllegalStateException("This point should never be reached, a place in texture should be available.");
            }
        } else {
            increaseSize();
            textures[numTextures] = texture;
            references[numTextures] = 0;
            return numTextures++;
        }
    }

    /**
     * Increases the texture and references array sizes by a third.
     */
    private void increaseSize()
    {
        final int newSize = (textures.length*4)/3;
        textures = ArrayUtil.copyOf(textures, newSize);
        references = ArrayUtil.copyOf(references, newSize);
    }

    /**
     * Removes the texture at the given index. It's positoin is then set to null.
     * @param index The index of the texture to remove.
     */
    final void removeTexture(final int index)
    {
        assert(index > 0);
        assert(index < textures.length);
        assert(textures[index] != null);
        
        textures[index] = null;
        --numTextures;
    }
    
    /**
     * Preloads the given texture so that it is stored in memory. When a texture
     * is created then if it is already loaded it then skips the loading process.
     * If this is called on a texture that already exists then it is reloaded.
     *
     * @param path The path of the texture to load.
     */
    public void load(String path)
    {
        setTexture(path, new GLTextureInner(path));
    }

    /**
     * If the given texture already exists in this texture manager then it
     * replaces the texture at that location. Otherwise it is added.
     * @param path The path name of the texture being added.
     * @param tex The texture being set at the given path.
     */
    private void setTexture(String path, GLTextureInner tex)
    {
        final Integer index = textureNames.get(path);

        if (index == null) {
            textureNames.put(path, addTexture(tex));
        } else {
            textures[index] = tex;
        }
    }

    /**
     * Returns the index for the texture stored at the given path. If the
     * texture is not found then it is loaded.
     * @param path The path to the texture to retrieve, cannot be null.
     * @return The index for where the given path to a texture is stored.
     */
    int newTexture(String path)
    {
        // error checking
        if (path == null) { throw new IllegalArgumentException("Path cannot be null"); }

        // get the texture
        Integer index = textureNames.get(path);
        if (index == null) {
            load(path);
            index = textureNames.get(path);
        }

        // reference and return texture
        final int intIndex = index.intValue();
        references[intIndex]++;
        return intIndex;
    }

    /**
     * Decrements the reference count for the texture at the given index. This
     * should be called when a GLTexture is no longer going to use the texture
     * at this position any more. This typically happens when the GLTexture is
     * recycled by the garbage collector.
     * 
     * If the reference count reaches 0 and auto unload is set then the texture
     * will be unloaded.
     * @param index The index of the texture to dereference.
     */
    void dereference(final int index)
    {
        assert(index > 0);
        assert(index < textures.length);
        assert(textures[index] != null);
        
        int referenceCount = --references[index];
        // it should never be negative
        assert(referenceCount >= 0);

        if (referenceCount == 0 && isAutoUnload()) {
            removeTexture(index);
        }
    }

    /**
     * @param path A path to the texture to check if it's loaded.
     * @return True if there is a texture already loaded for the given path. False if not.
     */
    public boolean isLoaded(String path)
    {
        return textureNames.containsKey(path);
    }

    /**
     * Reloads all of the textures that are currently loaded. This can cause
     * exceptions to be thrown if those textures are no longer where they were
     * when first loaded.
     */
    public void reload()
    {
        for (String tex : getTextureList()) {
            load(tex);
        }
    }

    /**
     * @return A list of all the textures that are currently loaded.
     */
    public Set<String> getTextureList()
    {
        return textureNames.keySet();
    }

    public boolean containsTexture(String path)
    {
        return textureNames.containsKey( path );
    }

    /**
     * Unloads all the textures that are not in use.
     * @throws IllegalArgumentException If the path to the given texture is not loaded, if the path is null or if the texture is still in use.
     */
    public void unload()
    {
        for (String tex : getTextureList()) {
            unload(tex);
        }
    }

    /**
     * Unloads the texture loaded from the given path if it is not in use.
     * @param path A path of the texture to unload.
     * @throws IllegalArgumentException If the path to the given texture is not loaded, if the path is null or if the texture is still in use.
     */
    public void unload(String path)
    {
        if (path == null) {
            throw new IllegalArgumentException("The given path is null.");
        }
        final Integer index = textureNames.remove( path );
        if (index == null) {
            throw new IllegalArgumentException("The given path was not found: " + path);
        }
        final int intIndex = index.intValue();
        if (references[intIndex] > 0) {
            throw new IllegalArgumentException("The texture: '" + path + "' is still in use by " + references[intIndex] + " textures.");
        }

        removeTexture(intIndex);
    }

    /**
     * Adds the given texture to be initialized later when initializeTextures()
     * is called. This allows textures to be loaded before the GL pipeline is
     * setup.
     * @param tex The texture to load in the future. Cannot be null.
     */
    void addInitializeTexture(GLTextureInner tex)
    {
        if (tex == null) {
            throw new IllegalArgumentException("The given texture cannot be null.");
        }
        textureInternalInitializeQueue.addLast(tex);
    }

    /**
     * Internally textures are queued to be loaded to allow them to appear to be
     * loaded at any time when really they are loaded only when the GL pipeline
     * exists and is available for use.
     * This method loads all the queued textures.
     *
     * If you are using a WorldContainer object then this should already be
     * handled for you.
     */
    void initializeTextures()
    {
        while (textureInternalInitializeQueue.size() > 0) {
            textureInternalInitializeQueue.removeFirst().initialize();
        }
    }
}
